feat: conversational Slack chat bots (inbox-buddy + joke-bot) + @mention wake-gate#85
Conversation
A Slack-channel chat agent that answers questions about recent Gmail over a
multi-turn conversation. Dogfooding forcing-function for conversational + email
threading.
- Human channel is Slack (a `slack` trigger watching one SLACK_CHANNEL), not the
relay inbox — the relay inbox is agent-to-agent and never fires from a human
Slack message. Mirrors the in-production linear-slack pattern. lib/slack.ts
- Reads Gmail from the relayfile VFS at /google-mail/threads/** (the real cloud
path; the legacy /gmail adapter path is unused). lib/gmail.ts
- Conversational continuity: persists the per-conversation transcript in
ctx.memory (workspace scope), keyed by the Slack thread/channel, and replays
it each turn (ctx.llm.complete is stateless). lib/conversation.ts
- Email threading: resolves "that thread with X" and reasons over the full
message list. lib/{gmail,prompt}.ts
- Replies in Slack; ignores bot/edit messages (loop guard).
- Golden tests prove multi-turn continuity + thread focus + slack gating + the
/google-mail scope invariant; chat eval cases incl. a multi-turn case; live
judge passes both. run-evals.mjs gains multi-turn `turns` support.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…unt) A sandbox:false (lightweight) delivery runs without a Daytona box, so the relayfile-mount daemon never runs and /google-mail reads come back empty. Match the in-production linear-slack agent (sandbox:true): the box mounts the VFS and the handler answers with ctx.llm.complete. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ent wake) Switch from the `message.created` Slack trigger to `app_mention`. `message.created` fires only on INGESTED message records, so a stalled relayfile slack sync (the migration) silently prevents the agent from ever waking. `app_mention` is webhook-driven — the message arrives in the event payload — so the wake-up is independent of the (stale) slack mount. Mirrors the in-production review-agent. SLACK_CHANNEL is now optional (handler-side restriction, no watch-path interp). Note: the wake-up is mount-independent, but the handler still reads the gmail mount for context and replies via slack writeback, both of which depend on relayfile being healthy — currently blocked by the relayfile migration (api.relayfile.dev returns empty revisions; mount sync frozen). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… agent) End-to-end proof bot for the conversational-agent pattern: one @mention in a Slack channel -> one joke reply in the same channel. Shares the inbox-buddy wake/guard plumbing (app_mention-driven, correct-channel reply guard, forward dedupe). Persona generated from persona.ts; persona.json + .workforce/ stay gitignored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… matching - .gitignore: ignore opencode.json (holds live relay creds), .agentworkforce/ (local CLI/handoff state) and .test-build/ (build output) so they can't be committed. - linear-slack: gate message.created wake on @mention so cloud only provisions a Daytona box when the agent is actually addressed (avoids sandbox-per-message). - review: useSubscription: true. - evals: substring log matching so enriched log lines still match expectations. - deps: @relayfile/relay-helpers 0.3.42 -> ^0.4.1. - daytona-monitor: add inbox-test fixture. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
📝 WalkthroughWalkthroughIntroduces two new Slack agents ( Changesinbox-buddy: Slack Gmail Agent
joke-bot: Slack Joke Agent
Eval harness multi-turn support and cross-cutting fixes
Sequence Diagram(s)sequenceDiagram
actor User
participant Slack
participant handleSlackMessage
participant loadConversation as loadConversation<br/>(ctx.memory)
participant loadRecentThreads as loadRecentThreads<br/>(VFS /google-mail/threads)
participant buildPrompt
participant LLM as ctx.llm.complete
participant recordTurn as recordTurn<br/>(ctx.memory)
User->>Slack: `@inbox-buddy` what happened with Alice?
Slack->>handleSlackMessage: app_mention event
handleSlackMessage->>loadConversation: recall transcript by conv key
loadConversation-->>handleSlackMessage: ConvTurn[]
handleSlackMessage->>loadRecentThreads: list threads JSON from VFS
loadRecentThreads-->>handleSlackMessage: GmailThread[]
handleSlackMessage->>buildPrompt: transcript + threads + question
buildPrompt-->>handleSlackMessage: composed prompt (overview + focused expansion)
handleSlackMessage->>LLM: complete(systemPrompt, userPrompt)
LLM-->>handleSlackMessage: answer text
handleSlackMessage->>recordTurn: persist updated transcript
handleSlackMessage->>Slack: postReply (thread or channel)
Slack-->>User: answer
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces two new conversational agents: inbox-buddy, which allows users to query their Gmail threads directly from Slack with multi-turn continuity, and joke-bot, a lightweight agent for generating jokes. It also optimizes the linear-slack agent trigger to wake only on mentions, and updates the evaluation script to support multi-turn chat scenarios. The review feedback highlights critical issues regarding Slack channel ID normalization across both new agents, where the presence of a __name suffix can cause silent failures, incorrect memory lookups, or API errors. Additionally, a regex issue in joke-bot was identified where all user mentions are stripped instead of just the leading bot mention.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| export function skipReason(msg: SlackMessage, boardChannel: string | undefined): string | null { | ||
| if (msg.isBot) return 'bot message'; | ||
| if (msg.subtype) return `slack subtype ${msg.subtype}`; | ||
| if (boardChannel && msg.channel !== boardChannel) return 'not the chat channel'; | ||
| if (!stripLeadingMention(msg.text).trim()) return 'empty message text'; | ||
| return null; | ||
| } |
There was a problem hiding this comment.
If either msg.channel or boardChannel contains the __name suffix (which is common in this platform's Slack payload/picker formats), the direct comparison msg.channel !== boardChannel will fail, causing the bot to silently ignore valid messages. Normalize both to bare IDs by splitting on __ before comparing.
export function skipReason(msg: SlackMessage, boardChannel: string | undefined): string | null {
if (msg.isBot) return 'bot message';
if (msg.subtype) return `slack subtype ${msg.subtype}`;
if (boardChannel) {
const chanId = msg.channel.split('__')[0];
const wantId = boardChannel.split('__')[0];
if (chanId !== wantId) return 'not the chat channel';
}
if (!stripLeadingMention(msg.text).trim()) return 'empty message text';
return null;
}| export async function postReply( | ||
| ctx: WorkforceCtx, | ||
| slack: SlackPoster, | ||
| msg: SlackMessage, | ||
| text: string | ||
| ): Promise<void> { | ||
| const result = msg.threadTs | ||
| ? await slack.reply(msg.channel, msg.threadTs, text) | ||
| : await slack.post(msg.channel, text); | ||
| if (!result?.ts) { | ||
| ctx.log?.('warn', 'inbox-buddy.reply.no-receipt', { channel: msg.channel, threaded: Boolean(msg.threadTs) }); | ||
| } | ||
| } |
There was a problem hiding this comment.
When posting a reply, using msg.channel directly might fail if it contains the __name suffix, as the Slack API/client expects a bare channel ID. Normalize the channel ID to its bare form before calling slack.reply or slack.post.
| export async function postReply( | |
| ctx: WorkforceCtx, | |
| slack: SlackPoster, | |
| msg: SlackMessage, | |
| text: string | |
| ): Promise<void> { | |
| const result = msg.threadTs | |
| ? await slack.reply(msg.channel, msg.threadTs, text) | |
| : await slack.post(msg.channel, text); | |
| if (!result?.ts) { | |
| ctx.log?.('warn', 'inbox-buddy.reply.no-receipt', { channel: msg.channel, threaded: Boolean(msg.threadTs) }); | |
| } | |
| } | |
| export async function postReply( | |
| ctx: WorkforceCtx, | |
| slack: SlackPoster, | |
| msg: SlackMessage, | |
| text: string | |
| ): Promise<void> { | |
| const chanId = msg.channel.split('__')[0]; | |
| const result = msg.threadTs | |
| ? await slack.reply(chanId, msg.threadTs, text) | |
| : await slack.post(chanId, text); | |
| if (!result?.ts) { | |
| ctx.log?.('warn', 'inbox-buddy.reply.no-receipt', { channel: msg.channel, threaded: Boolean(msg.threadTs) }); | |
| } | |
| } |
| export function conversationKeyForSlack(msg: SlackMessage): string { | ||
| return msg.threadTs ? `${msg.channel}:${msg.threadTs}` : msg.channel; | ||
| } |
There was a problem hiding this comment.
Normalize the channel ID to its bare form (splitting on __) when generating the conversation key to ensure consistency in memory lookups regardless of how the channel ID is formatted in the payload.
| export function conversationKeyForSlack(msg: SlackMessage): string { | |
| return msg.threadTs ? `${msg.channel}:${msg.threadTs}` : msg.channel; | |
| } | |
| export function conversationKeyForSlack(msg: SlackMessage): string { | |
| const chanId = msg.channel.split('__')[0]; | |
| return msg.threadTs ? `${chanId}:${msg.threadTs}` : chanId; | |
| } |
| const want = input(ctx, 'SLACK_CHANNEL'); | ||
| const chanId = channel.split('__')[0]; | ||
| if (!want) { | ||
| ctx.log?.('warn', 'joke-bot.slack-no-channel', { reason: 'SLACK_CHANNEL not set; failing closed' }); | ||
| return; | ||
| } | ||
| if (chanId !== want) { | ||
| ctx.log?.('info', 'joke-bot.slack-wrong-channel', { channel: chanId, want }); | ||
| return; | ||
| } |
There was a problem hiding this comment.
If SLACK_CHANNEL is configured with the id__name format (e.g., from a channel picker), the comparison chanId !== want will fail because chanId is normalized to a bare ID while want is not. Normalize want as well by splitting on __.
| const want = input(ctx, 'SLACK_CHANNEL'); | |
| const chanId = channel.split('__')[0]; | |
| if (!want) { | |
| ctx.log?.('warn', 'joke-bot.slack-no-channel', { reason: 'SLACK_CHANNEL not set; failing closed' }); | |
| return; | |
| } | |
| if (chanId !== want) { | |
| ctx.log?.('info', 'joke-bot.slack-wrong-channel', { channel: chanId, want }); | |
| return; | |
| } | |
| const want = input(ctx, 'SLACK_CHANNEL')?.split('__')[0]; | |
| const chanId = channel.split('__')[0]; | |
| if (!want) { | |
| ctx.log?.('warn', 'joke-bot.slack-no-channel', { reason: 'SLACK_CHANNEL not set; failing closed' }); | |
| return; | |
| } | |
| if (chanId !== want) { | |
| ctx.log?.('info', 'joke-bot.slack-wrong-channel', { channel: chanId, want }); | |
| return; | |
| } |
| const tag = `joke-convo:slack:${channel}:${threadTs}`; | ||
| const reply = await joke(ctx, buildPrompt(await recall(ctx, tag), question)); | ||
| const result = await slackClient({ writebackTimeoutMs: 15_000 }).reply(channel, threadTs, reply); | ||
| if (!result?.ts) throw new Error('Slack reply returned no receipt ts'); | ||
| await remember(ctx, tag, question, reply); | ||
| ctx.log?.('info', 'joke-bot.slack-replied', { channel, threadTs, chars: reply.length }); |
There was a problem hiding this comment.
Use the normalized chanId instead of channel when calling slack.reply to prevent potential Slack API errors if the channel ID contains the __name suffix.
| const tag = `joke-convo:slack:${channel}:${threadTs}`; | |
| const reply = await joke(ctx, buildPrompt(await recall(ctx, tag), question)); | |
| const result = await slackClient({ writebackTimeoutMs: 15_000 }).reply(channel, threadTs, reply); | |
| if (!result?.ts) throw new Error('Slack reply returned no receipt ts'); | |
| await remember(ctx, tag, question, reply); | |
| ctx.log?.('info', 'joke-bot.slack-replied', { channel, threadTs, chars: reply.length }); | |
| const tag = `joke-convo:slack:${chanId}:${threadTs}`; | |
| const reply = await joke(ctx, buildPrompt(await recall(ctx, tag), question)); | |
| const result = await slackClient({ writebackTimeoutMs: 15_000 }).reply(chanId, threadTs, reply); | |
| if (!result?.ts) throw new Error('Slack reply returned no receipt ts'); |
| async function handleJokeOfTheDay(ctx: WorkforceCtx): Promise<void> { | ||
| const channel = input(ctx, 'SLACK_CHANNEL'); | ||
| if (!channel) { | ||
| ctx.log?.('warn', 'joke-bot.no-channel', { reason: 'SLACK_CHANNEL not set; skipping joke of the day' }); | ||
| return; | ||
| } | ||
| const reply = await joke(ctx, 'Give me one short, original "joke of the day" about recent tech / pop-culture / current events.'); | ||
| const result = await slackClient({ writebackTimeoutMs: 15_000 }).post(channel, `🃏 Joke of the day:\n${reply}`); | ||
| if (!result?.ts) throw new Error('Slack post returned no receipt ts for joke of the day'); | ||
| ctx.log?.('info', 'joke-bot.jotd-posted', { channel }); | ||
| } |
There was a problem hiding this comment.
Normalize the channel ID to its bare form before calling slack.post to ensure compatibility with the Slack API if SLACK_CHANNEL is configured with the id__name format.
| async function handleJokeOfTheDay(ctx: WorkforceCtx): Promise<void> { | |
| const channel = input(ctx, 'SLACK_CHANNEL'); | |
| if (!channel) { | |
| ctx.log?.('warn', 'joke-bot.no-channel', { reason: 'SLACK_CHANNEL not set; skipping joke of the day' }); | |
| return; | |
| } | |
| const reply = await joke(ctx, 'Give me one short, original "joke of the day" about recent tech / pop-culture / current events.'); | |
| const result = await slackClient({ writebackTimeoutMs: 15_000 }).post(channel, `🃏 Joke of the day:\n${reply}`); | |
| if (!result?.ts) throw new Error('Slack post returned no receipt ts for joke of the day'); | |
| ctx.log?.('info', 'joke-bot.jotd-posted', { channel }); | |
| } | |
| async function handleJokeOfTheDay(ctx: WorkforceCtx): Promise<void> { | |
| const channel = input(ctx, 'SLACK_CHANNEL'); | |
| if (!channel) { | |
| ctx.log?.('warn', 'joke-bot.no-channel', { reason: 'SLACK_CHANNEL not set; skipping joke of the day' }); | |
| return; | |
| } | |
| const chanId = channel.split('__')[0]; | |
| const reply = await joke(ctx, 'Give me one short, original "joke of the day" about recent tech / pop-culture / current events.'); | |
| const result = await slackClient({ writebackTimeoutMs: 15_000 }).post(chanId, `🃏 Joke of the day:\n${reply}`); | |
| if (!result?.ts) throw new Error('Slack post returned no receipt ts for joke of the day'); | |
| ctx.log?.('info', 'joke-bot.jotd-posted', { channel: chanId }); | |
| } |
| const channel = input(ctx, 'SLACK_CHANNEL'); | ||
| if (!channel) { | ||
| ctx.log?.('warn', 'joke-bot.no-channel', { reason: 'SLACK_CHANNEL not set; cannot reply' }); | ||
| return; | ||
| } | ||
| const question = await readQuestion(event); | ||
| if (!question) { | ||
| ctx.log?.('info', 'joke-bot.empty', { reason: 'no text in message; skipping' }); | ||
| return; | ||
| } | ||
| const tag = `joke-convo:${channel}`; | ||
| const reply = await joke(ctx, buildPrompt(await recall(ctx, tag), question)); | ||
| const result = await slackClient({ writebackTimeoutMs: 15_000 }).post(channel, reply); | ||
| if (!result?.ts) throw new Error('Slack post returned no receipt ts'); | ||
| await remember(ctx, tag, question, reply); | ||
| ctx.log?.('info', 'joke-bot.replied', { channel, surface: 'relay', chars: reply.length }); |
There was a problem hiding this comment.
Normalize the channel ID to its bare form before using it for memory tags and posting to Slack to ensure consistency and prevent API errors if SLACK_CHANNEL contains the __name suffix.
const channel = input(ctx, 'SLACK_CHANNEL');
if (!channel) {
ctx.log?.('warn', 'joke-bot.no-channel', { reason: 'SLACK_CHANNEL not set; cannot reply' });
return;
}
const chanId = channel.split('__')[0];
const question = await readQuestion(event);
if (!question) {
ctx.log?.('info', 'joke-bot.empty', { reason: 'no text in message; skipping' });
return;
}
const tag = `joke-convo:${chanId}`;
const reply = await joke(ctx, buildPrompt(await recall(ctx, tag), question));
const result = await slackClient({ writebackTimeoutMs: 15_000 }).post(chanId, reply);
if (!result?.ts) throw new Error('Slack post returned no receipt ts');
await remember(ctx, tag, question, reply);
ctx.log?.('info', 'joke-bot.replied', { channel: chanId, surface: 'relay', chars: reply.length });| return; | ||
| } | ||
| const threadTs = typeof data.thread_ts === 'string' && data.thread_ts ? data.thread_ts : ts; | ||
| const question = rawText.replace(/<@[^>]+>/g, '').trim(); |
There was a problem hiding this comment.
Using a global regex /<@[^>]+>/g will strip all user mentions from the message, including mentions of other users (e.g., @joke-bot tell a joke to @alice becomes tell a joke to). Only strip the leading mention to preserve other mentions in the message.
| const question = rawText.replace(/<@[^>]+>/g, '').trim(); | |
| const question = rawText.replace(/^\s*<@[^>]+>\s*/, '').trim(); |
|
Caution Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted. Error details |
| // Slack is the reply surface (writeback to /slack/channels/{id}/messages), so | ||
| // scope channels. An unscoped slack mount would make post() a silent no-op. | ||
| integrations: { | ||
| slack: { scope: { channels: '/slack/channels/**' } } |
There was a problem hiding this comment.
🔴 joke-bot Slack scope uses wrong key channels instead of paths, likely making writebacks silently fail
The joke-bot/persona.ts Slack integration scope uses { channels: '/slack/channels/**' }, but every other agent in the repo (9+ agents) consistently uses { paths: '/slack/channels/**' } for the Slack scope key. The comment on lines 66-67 explicitly warns: "An unscoped slack mount would make post() a silent no-op." Using the unrecognized key channels instead of paths likely means the Slack mount is effectively unscoped, causing slackClient().post() and .reply() to silently drop all messages — the bot generates a joke but the reply never reaches Slack.
All other agents use `paths` for Slack scope
vendor-monitor/persona.ts:17, linear-slack/persona.ts:41, hn-monitor/persona.ts:19, spotify-releases/persona.ts:18, neon-monitor/persona.ts:63, daytona-monitor/persona.ts:35, review/persona.ts:35, inbox-buddy/persona.ts:52, repo-hygiene/persona.ts:24, gcp-watcher/persona.ts:45 — all use scope: { paths: '/slack/channels/**' }.
| slack: { scope: { channels: '/slack/channels/**' } } | |
| slack: { scope: { paths: '/slack/channels/**' } } |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3cbad0dead
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Gmail threads materialize under /google-mail (provider id `google-mail`). | ||
| // `/google-mail/**` mounts the threads + LAYOUT.md the handler reads. An | ||
| // unscoped mirror (or a `/gmail/**` scope) would mount nothing. | ||
| 'google-mail': { scope: { paths: '/google-mail/**' } }, |
There was a problem hiding this comment.
Scope Gmail below the provider root
This grants the provider-root glob /google-mail/**. The existing linear-slack/persona.ts notes that cloud's mirror drops provider-root globs via isProviderRootPath, so inbox-buddy's sandbox can be deployed without the /google-mail/threads files it reads; loadRecentThreads() then catches the failure and returns [], making the bot claim no Gmail is visible. Scope the concrete subtree the handler needs, such as /google-mail/threads/** (and LAYOUT.md separately if needed).
Useful? React with 👍 / 👎.
| // ── persona config invariant (§1 scope trap) ────────────────────────────────── | ||
|
|
||
| test('compiled persona scopes the REAL gmail mount (/google-mail, not legacy /gmail) + slack', async () => { | ||
| const persona = JSON.parse(readFileSync(path.join(HERE, '..', 'inbox-buddy', 'persona.json'), 'utf8')); |
There was a problem hiding this comment.
Read the compiled persona from .test-build
In a fresh checkout npm test only runs tsc --outDir .test-build; it does not generate the ignored inbox-buddy/persona.json file. This assertion therefore fails unless the developer happened to run npm run compile beforehand, so the new inbox-buddy test suite is not reproducible from the documented test command. Import .test-build/inbox-buddy/persona.js here, or have the test setup compile persona JSON first.
Useful? React with 👍 / 👎.
…review) Addresses Gemini review feedback on PR #85: - joke-bot/inbox-buddy: normalize Slack channel ids to bare form (strip the `__name` suffix the platform appends in some payloads) before comparing the channel guard, keying memory, and calling the Slack API. Added a shared bareChannelId() helper in inbox-buddy/lib/slack.ts. Safe no-op on bare ids. - joke-bot: strip ONLY the leading bot mention (`/^\s*<@[^>]+>\s*/`) instead of a global `/<@[^>]+>/g`, so other mentions in the message text are preserved. Also fixes a pre-existing typecheck break that stopped `npm test` from running: inbox-buddy's app_mention handler event type isn't in the runtime's exported event unions, so cast across that type-defs gap at the call site. Typecheck now clean; full suite (142 tests) green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Addressed the review feedback in 4c5afcc:
Also fixed a pre-existing typecheck break (unrelated to the review) that was stopping |
|
pr-reviewer could not complete review for #85 in AgentWorkforce/agents. |
|
ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed. pr-reviewer could not complete review for #85 in AgentWorkforce/agents. |
There was a problem hiding this comment.
Actionable comments posted: 13
🧹 Nitpick comments (1)
joke-bot/persona.ts (1)
14-16: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winAlign persona documentation/config comments with the actual generation path.
Lines 14-16 and 40-42 describe harness-based joke generation, but the handler currently uses
ctx.llm.complete. This mismatch will mislead debugging and ops expectations.Also applies to: 40-43
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@joke-bot/persona.ts` around lines 14 - 16, The comments at lines 14-16 and 40-42 in persona.ts incorrectly document the joke generation path as using ctx.harness.run, but the actual handler implementation uses ctx.llm.complete instead. Update these comments to accurately reflect that jokes are generated via ctx.llm.complete, removing the misleading reference to ctx.harness.run and the incorrect description about testing different provider behaviors through harness switching. This will ensure the documentation aligns with the actual implementation and prevents future debugging confusion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@evals/cases.jsonl`:
- Around line 12-13: The test cases inbox-buddy.chat and
inbox-buddy.chat-multiturn currently only verify success status, side effects,
and log output in their expect blocks, but do not assert anything about the
actual Slack reply text content. This means a hallucinated or incorrect reply
would still pass the test. Add a machine-checked assertion field (such as
replyContains or similar validation mechanism) to the expect object in both test
cases that verifies the Slack reply text includes the specific facts mentioned
in each case's rubric: for inbox-buddy.chat, assert the reply mentions "final
numbers by Friday" and "finance"; for inbox-buddy.chat-multiturn, assert the
second turn's reply mentions "finance@acme.com" and "Friday" to prove context
was retained from the first turn.
In `@evals/seeds/gmail-thread-newsletter.json`:
- Around line 13-17: The internalDate field (line 13) and date field (line 17)
in the gmail-thread-newsletter.json seed file represent different timestamps for
the same message, which can cause incorrect ordering in tests that depend on
internalDate. Convert the human-readable date string "Wed, 03 Jun 2026 12:00:00
-0700" to its corresponding Unix timestamp in milliseconds and update the
internalDate value to match this converted timestamp, ensuring both fields
represent the exact same point in time.
In `@inbox-buddy/lib/conversation.ts`:
- Around line 57-62: The saveConversation function currently allows errors from
ctx.memory.save to propagate and abort the chat flow. Wrap the ctx.memory.save
call in a try-catch block in the saveConversation function to catch any errors,
log them for visibility, and then return gracefully without throwing. This makes
the transcript persistence best-effort rather than request-fatal, allowing the
chat flow to continue even if memory persistence temporarily fails, which aligns
with the tolerant error handling pattern already used in loadConversation.
In `@inbox-buddy/README.md`:
- Around line 23-26: The README documentation at lines 23-26 and 53-56 indicates
that SLACK_CHANNEL is a required parameter used for trigger-path interpolation,
but the actual implementation treats it as an optional parameter that is only
used when explicitly set and gated within the handler logic. Update the README
to clarify that SLACK_CHANNEL is optional rather than required, remove the
implication about trigger-path interpolation if it is not actually used in that
manner, and explain that the parameter is conditionally used by the
persona/handler when provided.
- Around line 7-12: The fenced code block containing the conversation example
(starting with "You → `#your-channel`: What's the latest on the Q3 export thread
with Alice?") is missing a language identifier on the opening fence, which
violates the MD040 markdown rule. Add the language identifier "text" immediately
after the opening triple backticks (```) to specify the content type and resolve
the linting error.
In `@joke-bot/agent.ts`:
- Around line 146-147: The channel ID is normalized to chanId by extracting the
first part before the double underscore at lines 146-147, but the raw channel
variable is still being used in the memory tag and Slack reply operations at
lines 171 and 173. Replace all uses of the raw channel variable with the
normalized chanId variable in both the memory tag operation and the reply call
to ensure consistent channel ID handling throughout the function.
- Around line 121-122: The memory key tag on line 121 uses only the channel to
create the relay memory key, causing all conversations within that channel to
share the same memory stream and potentially leak information between users.
Modify the tag construction to include a unique conversation or user identifier
in addition to the channel variable so that each conversation has its isolated
memory. This ensures the recall function on line 122 retrieves only the relevant
conversation history for that specific user or DM thread, not shared workspace
memory.
- Around line 51-57: The readQuestion function is missing a check for
resource.text, which is where relay message payloads store the text field.
Currently the function only checks data?.text and nested.text, causing relay
messages to be treated as empty strings. Add resource?.text as a fallback option
in the text extraction logic after the nested.text check so that the function
can properly extract text from relay event payloads where data is absent but
resource.text is available.
- Around line 160-166: The regex pattern /<@[^>]+>/ in the mention gate check is
too broad as it matches any user mention in the message, not just bot
invocations. Replace this pattern to specifically match either a leading
invocation token at the start of the message or the bot's specific mention ID to
ensure the bot only responds when explicitly invoked, not when other users are
mentioned in unrelated conversations.
In `@scripts/evals/run-evals.mjs`:
- Around line 270-278: The issue is that when the final turn in a multi-turn
conversation produces no model output, the eval system silently selects the last
available reply from a previous turn instead of detecting this failure. To fix
this, modify the logic where `replies[replies.length - 1]` is selected (around
lines 341-344) to validate that the selected reply actually comes from the final
turn and that no turns were skipped due to missing output. Track which turn each
reply in the replies array corresponds to, or verify that the replies array
length matches the expected number of turns executed, so if the final turn
produced no output, the eval properly detects and reports this failure instead
of silently reusing a prior turn's result.
- Around line 95-101: In the multi-turn scenario where env.resource is merged
(line 95), the subsequent checks for promoting channel and messageId (lines
96-97) only look at the turn object. After the merge, also check the resulting
env.resource object itself for channel and messageId properties and promote them
to top-level env fields if they exist there. This ensures that channel/messageId
values carried in the fixture.resource are properly promoted even when omitted
from individual turns.
- Around line 328-330: The RELAYFILE_MOUNT_ROOT and WORKSPACE_ROOT environment
variables are being set directly on process.env at lines 328-329 without
restoration, causing these values to leak into subsequent test cases. Instead of
modifying process.env directly, save the original values of these environment
variables before the withCaseEnv call, then restore them in a finally block
after the async operation completes to ensure proper cleanup between test cases.
In `@tests/inbox-buddy.test.mjs`:
- Around line 189-225: The test in the multi-turn function currently deletes
process.env.RELAYFILE_MOUNT_ROOT in the finally block without restoring any
previously existing value. Save the original value of
process.env.RELAYFILE_MOUNT_ROOT before assigning the mount value (before
entering the try block), then in the finally block restore the saved original
value instead of using delete, so that if the env var was set before the test
ran, that value is preserved for subsequent tests.
---
Nitpick comments:
In `@joke-bot/persona.ts`:
- Around line 14-16: The comments at lines 14-16 and 40-42 in persona.ts
incorrectly document the joke generation path as using ctx.harness.run, but the
actual handler implementation uses ctx.llm.complete instead. Update these
comments to accurately reflect that jokes are generated via ctx.llm.complete,
removing the misleading reference to ctx.harness.run and the incorrect
description about testing different provider behaviors through harness
switching. This will ensure the documentation aligns with the actual
implementation and prevents future debugging confusion.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 83a320b1-eac5-4244-861a-a463124f7bcc
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (22)
.gitignoredaytona-monitor/fixtures/inbox-test.jsonevals/cases.jsonlevals/seeds/gmail-thread-alice-export.jsonevals/seeds/gmail-thread-bob-lunch.jsonevals/seeds/gmail-thread-github-pr.jsonevals/seeds/gmail-thread-newsletter.jsoninbox-buddy/README.mdinbox-buddy/agent.tsinbox-buddy/lib/conversation.tsinbox-buddy/lib/gmail.tsinbox-buddy/lib/prompt.tsinbox-buddy/lib/slack.tsinbox-buddy/persona.tsjoke-bot/agent.tsjoke-bot/fixtures/inbox-joke.jsonjoke-bot/persona.tslinear-slack/agent.tspackage.jsonreview/persona.tsscripts/evals/run-evals.mjstests/inbox-buddy.test.mjs
| process.env.RELAYFILE_MOUNT_ROOT = mount; | ||
| process.env.WORKSPACE_ROOT = mount; | ||
| await withCaseEnv(personaSpec, testCase.inputs ?? {}, {}, () => handler(ctx, event)); | ||
| await withCaseEnv(personaSpec, testCase.inputs ?? {}, {}, async () => { |
There was a problem hiding this comment.
Avoid leaking mount env vars across test cases.
Lines 328-329 set process env directly and never restore previous values. In a multi-case run, that can bleed mount state across cases.
Suggested fix
- process.env.RELAYFILE_MOUNT_ROOT = mount;
- process.env.WORKSPACE_ROOT = mount;
- await withCaseEnv(personaSpec, testCase.inputs ?? {}, {}, async () => {
+ await withCaseEnv(
+ personaSpec,
+ testCase.inputs ?? {},
+ { RELAYFILE_MOUNT_ROOT: mount, WORKSPACE_ROOT: mount },
+ async () => {
for (let i = 0; i < events.length; i++) {
const event = events[i];
if (!event) throw new Error(`envelopeToAgentEvent returned null for turn ${i} (unsupported envelope)`);
await handler(ctx, event);
}
- });
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| process.env.RELAYFILE_MOUNT_ROOT = mount; | |
| process.env.WORKSPACE_ROOT = mount; | |
| await withCaseEnv(personaSpec, testCase.inputs ?? {}, {}, () => handler(ctx, event)); | |
| await withCaseEnv(personaSpec, testCase.inputs ?? {}, {}, async () => { | |
| await withCaseEnv( | |
| personaSpec, | |
| testCase.inputs ?? {}, | |
| { RELAYFILE_MOUNT_ROOT: mount, WORKSPACE_ROOT: mount }, | |
| async () => { | |
| for (let i = 0; i < events.length; i++) { | |
| const event = events[i]; | |
| if (!event) throw new Error(`envelopeToAgentEvent returned null for turn ${i} (unsupported envelope)`); | |
| await handler(ctx, event); | |
| } | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/evals/run-evals.mjs` around lines 328 - 330, The RELAYFILE_MOUNT_ROOT
and WORKSPACE_ROOT environment variables are being set directly on process.env
at lines 328-329 without restoration, causing these values to leak into
subsequent test cases. Instead of modifying process.env directly, save the
original values of these environment variables before the withCaseEnv call, then
restore them in a finally block after the async operation completes to ensure
proper cleanup between test cases.
|
pr-reviewer could not complete review for #85 in AgentWorkforce/agents. |
1 similar comment
|
pr-reviewer could not complete review for #85 in AgentWorkforce/agents. |
|
ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed. pr-reviewer could not complete review for #85 in AgentWorkforce/agents. |
joke-bot, the linear-slack @mention gate, and the relay-helpers ^0.4.1 bump already landed on main via #83/#84. Resolve the add/add conflicts in favor of this branch (identical content for all except joke-bot/agent.ts, where this branch additionally carries the PR #85 review fixes: channel id normalization + leading-only mention strip). Net-new here remains inbox-buddy + those fixes.
…eval rigor) - persona: scope the concrete /google-mail/threads/** subtree (+ LAYOUT.md) instead of the provider-root glob /google-mail/**. Cloud's mount drops provider-root globs (isProviderRootPath), so /google-mail/** mirrored nothing and the bot reported no Gmail. Matches the linear-slack precedent. (Codex P1) - conversation: make saveConversation best-effort (log + continue) so a transient ctx.memory.save outage degrades continuity instead of aborting the reply. (CodeRabbit Major) - test: read the tsc-compiled persona module from .test-build instead of the gitignored persona.json, so the persona-scope test is reproducible from `npm test` alone; assertions updated for the new scope shape. (Codex P2) - evals: add a machine-checked `replyContains` assertion (case-insensitive, enforced only when a real reply exists / live runs) and grounded-fact assertions to the two inbox-buddy chat cases, so a hallucinated reply fails without the LLM judge. (CodeRabbit Major) - seed: align gmail-thread-newsletter internalDate with its date timestamp. - README: add a language to the example fenced block. Typecheck clean; full suite 142/142 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…accuracy) - run-evals: promote channel/messageId from the MERGED resource so a multi-turn turn that omits them inherits from fixture.resource (CodeRabbit Major). - run-evals: only judge a reply the FINAL turn actually produced — return null instead of silently reusing an earlier turn's output, so a failed last turn isn't masked (CodeRabbit Major). - tests: save/restore RELAYFILE_MOUNT_ROOT instead of deleting it, so a runner-set value doesn't leak away between cases (CodeRabbit Minor). - README: SLACK_CHANNEL is optional and app_mention is webhook-driven (not interpolated into a watch path) — table was out of sync (CodeRabbit Minor). Typecheck clean; full suite 142/142 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Conflicts resolved (merged Review feedback addressed across the last two pushes: Codex
CodeRabbit
Gemini (slack.ts channel normalization, lines 94/107/124): already addressed in 4c5afcc via the shared Also added the generated agent graphics (avatar/card/banner) for the four agents that were missing them: gcp-watcher, inbox-buddy, joke-bot, neon-monitor. Typecheck clean; full suite 142/142 green. |
|
ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed. Review of PR #85 —
|
Adopt the server-side threading pattern from internal-agents x-reply-radar: post
a compact `:newspaper: *Hacker News* — N new match(es)` header, then post the
digest body with `replyTo: <header ref>` so the cloud orders it after the header
delivers and sets thread_ts itself — no parent-receipt round-trip. Keeps the
channel scannable (header only) with detail in-thread.
- summarize() now returns { header, body } so the header can be the parent.
- Idempotency: claim "seen" before posting (at-least-once concurrency guard,
cloud#1990); once the header lands, never throw out of the post block (a retry
would re-post a duplicate header) — release the claim + rethrow only while
nothing has posted yet, otherwise log loudly.
- no-receipt (ts:'') on either post is still a loud failure (silent-drop guard).
- tests updated for the two-message threaded shape (header + replyTo body).
Typecheck clean; full suite 142/142 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Guard against malformed/old-schema `hn-monitor:post` records in the Q&A path: optional-chain postedAt/digest in the prompt context, default stories→[] and title/url in the fallback titles, and sort with localeCompare on a string fallback so a missing postedAt can't crash or misorder. (Gemini review, skills#80) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
pr-reviewer could not complete review for #85 in AgentWorkforce/agents. |
|
ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed. Review: PR #85 —
|
What
Lands the conversational Slack chat-bot work and supporting agent-infra hardening.
Conversational chat bots
app_mention-driven wake (webhook-driven, mount-independent),sandbox:true(lightweight delivery skips the VFS mount).@mentionin a Slack channel → one joke reply in the right channel. Exercises the same wake/guard/dedupe plumbing end-to-end.persona.json+.workforce/stay gitignored (generated frompersona.ts).Infra / tooling
.gitignorehardening: ignoreopencode.json(held live relay credentials locally — never committed, now can't be),.agentworkforce/(local CLI/handoff state), and.test-build/(build output).message.createdwake on@mentionso cloud only provisions a Daytona box when the agent is actually addressed (avoids sandbox-per-message waste).useSubscription: true.@relayfile/relay-helpers0.3.42→^0.4.1.inbox-testfixture.Notes
sandbox:falsedoes not skip the Daytona box (cloud#2418), so replies are ~minutes. Boxless exec is being pursued separately as a follow-up.🤖 Generated with Claude Code
Summary by cubic
Adds two conversational Slack bots:
inbox-buddychats about Gmail with multi‑turn memory, andjoke-botproves the same @mention pattern with quick replies. Also threadshn-monitordigests under a count header and hardens its Q&A path with defensive reads.New Features
inbox-buddy:slack.app_mentionwake; reads/google-mail/threads/**; per‑thread/channel memory inctx.memory; grounded replies;sandbox: true.joke-bot: threaded@mentionreplies in the configured channel, supports relay inbox DM, per‑thread memory, and a scheduled “joke of the day”;sandbox: false.linear-slack: gatemessage.createdwakes on@mentionto avoid provisioning a box per message.Refactors
/google-mail/threads/**(+/google-mail/LAYOUT.md) so threads mount; avoid the dropped provider‑root glob.savebest‑effort so replies don’t fail on transient memory errors.@mention; use bare ids for replies.hn-monitor: post a top‑level count header, then thread the digest withreplyTo; keep seen‑idempotency, fall back to a plain digest on LLM timeout/error, fail loudly on missing Slack writebacks, and use defensive reads for stored post records (optional‑chain fields, default empty arrays, safe sort onpostedAt).turnswith sharedctx.memory; substring log matching;replyContainsgrounding checks; only judge the final turn’s reply; promotechannel/messageIdfrom the merged resource in multi‑turn cases.RELAYFILE_MOUNT_ROOTin tests; clarifySLACK_CHANNELis optional andapp_mentionis webhook‑driven; add “Launch Agent” buttons and README banners..gitignore: ignoreopencode.json,.agentworkforce/,.test-build/.review: setuseSubscription: true.daytona-monitor: addinbox-testfixture.@relayfile/relay-helpersto^0.4.1.Written for commit 96ea19e. Summary will update on new commits.